package com.webapi.core.interceptor;

import static com.webapi.Constants.Common.AGENT;
import static com.webapi.Constants.Common.SIGN;
import static com.webapi.Constants.Common.TIMESTAMP;
import static com.webapi.Constants.Common.VERSION;
import static com.webapi.Constants.Http.CONTENT_SIGNATURE;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.webapi.Constants;
import com.webapi.common.ErrorCode;
import com.webapi.common.ErrorMsg;
import com.webapi.common.utils.JsonMapper;
import com.webapi.common.utils.MD5;
import com.webapi.common.utils.Parameters;
import com.webapi.core.model.common.AgentStatus;
import com.webapi.core.model.persist.Agent;
import com.webapi.core.model.persist.RequestLog;
import com.webapi.core.service.FacadeService;

public class SignatureInterceptor extends HandlerInterceptorAdapter {
	
	private final Log logger = LogFactory.getLog(getClass());

    @Autowired
    private FacadeService facadeService;
    @Autowired
    private JsonMapper jsonMapper;

    private Map<String, String> headerMap = null;
    private String contentBody = null;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            header(request);
            sign(request);
            saveRequest(request);
            return true;
        } catch (IllegalArgumentException e) {
            result(request, response, HttpStatus.BAD_REQUEST, e);
        } catch (SecurityException e) {
            result(request, response, HttpStatus.UNAUTHORIZED, e);
        } catch (UnsupportedOperationException e) {
            result(request, response, HttpStatus.FORBIDDEN, e);
        }
        return false;
    }

    private void result(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Exception e)
            throws IOException {
        response.setStatus(status.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ErrorMsg errorMsg = new ErrorMsg(request.getRequestURI().replace(request.getContextPath(), ""), e.getMessage());
        Writer out = response.getWriter();
        out.write(jsonMapper.writeValueAsString(errorMsg));
        out.flush();
        out.close();
    }

    private void header(HttpServletRequest req) {
        transHeaderMap(req);
        checkHeader(new String[] { AGENT, VERSION, SIGN, TIMESTAMP });
    }

    private void transHeaderMap(HttpServletRequest req) {
        String[] signatures = headerRequestValue(req, CONTENT_SIGNATURE).split(";");
        headerMap = new HashMap<String, String>();
        for (String s : signatures) {
            String[] vos = s.split("=");
            headerMap.put(vos[0], vos[1]);
        }
    }

    private void sign(HttpServletRequest req) {
        signValidate(headerMapValue(SIGN), buildKey(req));
    }

    private void signValidate(String sign, String key) {
        if (!sign.equals(MD5.code(key))) {
            throw new IllegalArgumentException(ErrorCode.System.S10101);
        }
    }

    private String buildKey(HttpServletRequest req) {
        List<String> paramsList = new ArrayList<String>();
        paramsList.addAll(headerMap.keySet());
        paramsList.remove(SIGN);
        Collections.sort(paramsList);

        return toRule(req, paramsList).toLowerCase();
    }

    private String toRule(HttpServletRequest req, List<String> paramsList) {
    	String secret = getSecret(req);
        String body = readBody(req);
        
        StringBuffer sb = new StringBuffer();
        sb.append(secret);
        for (String key : paramsList) {
            sb.append(key).append(headerMap.get(key));
        }
        sb.append(body);
        sb.append(secret);
        return sb.toString().trim();
    }

    private String getSecret(HttpServletRequest req) {
        String agentCode = headerMapValue(AGENT);
        Agent agent = Parameters.INSTANCE.getAgentMap().get(agentCode);
        checkAgent(req, agent);
        return agent.getSecret();
    }

    private String readBody(HttpServletRequest req) {
    	try {
    		ServletInputStream sis = req.getInputStream();
    		BufferedReader is = new BufferedReader(new InputStreamReader(sis, "UTF-8"));
			contentBody = is.readLine();
			return contentBody;
		} catch (UnsupportedEncodingException e) {
			logger.error("Unsupported encoding", e);
			throw new IllegalArgumentException(ErrorCode.System.S10009);
		} catch (IOException e) {
			logger.error("Read content body", e);
			throw new IllegalArgumentException(ErrorCode.System.S10010);
		} 
    }
    
    private void checkAgent(HttpServletRequest req, Agent agent) {
        if (null == agent) {
            throw new IllegalArgumentException(ErrorCode.System.S10103);
        } else if (agent.getStatus().equals(AgentStatus.Invalid)) {
            throw new SecurityException(ErrorCode.System.S10102);
        } else if (agent.getStatus().equals(AgentStatus.Test)) {
            if (req.getRequestURI().toLowerCase().contains("book")) {
                throw new UnsupportedOperationException(ErrorCode.System.S10005);
            }
        }
    }

    private void checkHeader(String[] hs) {
        for (String s : hs) {
            headerMapValue(s);
        }
    }

    private String headerRequestValue(HttpServletRequest req, String s) {
        return checkNull(s, req.getHeader(s));
    }

    private String headerMapValue(String s) {
        return checkNull(s, headerMap.get(s));
    }

    private String checkNull(String s, String value) {
        if (StringUtils.isBlank(value)) {
            throw new IllegalArgumentException(String.format(ErrorCode.System.S10006, s));
        } else if (s.equalsIgnoreCase(TIMESTAMP)) {
            checkTimeStamp(s, value);
        }
        return value;
    }

    private void checkTimeStamp(String s, String value) {
        DateFormat format = new SimpleDateFormat(Constants.DateFormatter.LONG_NO);
        Date timeStamp = null;
        try {
            timeStamp = format.parse(value);
        } catch (ParseException e) {
            throw new IllegalArgumentException(String.format(ErrorCode.System.S10006, s));
        }
        Calendar stamp = Calendar.getInstance();
        stamp.setTime(timeStamp);
        checkTimeSeperate(s, stamp);
    }

    private void checkTimeSeperate(String s, Calendar stamp) {
        Calendar ago = Calendar.getInstance();
        ago.set(Calendar.MINUTE, ago.get(Calendar.MINUTE) - 3);
        Calendar after = Calendar.getInstance();
        after.set(Calendar.MINUTE, after.get(Calendar.MINUTE) + 3);
        if (stamp.before(ago) || stamp.after(after)) {
            throw new IllegalArgumentException(String.format(ErrorCode.System.S10007, s));
        }
    }
    
    private void saveRequest(HttpServletRequest req) {
        String url = req.getRequestURL().toString();
        String[] objs = req.getRequestURI().replace(req.getContextPath(), "").split("\\/");

        RequestLog log = new RequestLog();
        log.setCode(headerMapValue(AGENT));
        log.setIp(req.getLocalAddr());
        log.setCategory(objs[1]);
        log.setMethod(objs[2]);
        log.setType(req.getHeader(Constants.Http.CONTENT_TYPE));
        log.setData(contentBody);
        log.setUri(url);

        facadeService.saveRequest(log);
    }

}
